
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class ThreeLayerNNet implements
java.io.Serializable {

	/**
	 * 
	 */
	private static final long serialVersionUID = 6324834228072712681L;
	/**
	 * @param args
	 */
	private double[] InputLayer;
	private double[] HiddenLayer;
	private double[] OutputLayer;
	
	private double[][] IHWeights;
	private double[][] HOWeights;

	private double learningSpeed = 0.1;
	private boolean isBiased;
	private boolean hiddenSigmoid;
	private boolean outputSigmoid;
	double[] OutputError;
	double[] HiddenError;
    int i,j = 0;
    
	
	public ThreeLayerNNet(int nrOfInputNeurons, int nrOfHiddenNeurons, int nrOfOutputNeurons, double aLearningSpeed, boolean aIsBiased, boolean aHiddenSigmoid, boolean aOutputSigmoid){
		InputLayer = new double[nrOfInputNeurons +1];
		HiddenLayer = new double[nrOfHiddenNeurons +1];
		OutputLayer = new double[nrOfOutputNeurons];
		IHWeights = new double[nrOfInputNeurons+1][nrOfHiddenNeurons];
		HOWeights = new double[nrOfHiddenNeurons+1][nrOfOutputNeurons];
		initializeWeights();
		
		setLearningSpeed(aLearningSpeed);
		isBiased = aIsBiased;
		hiddenSigmoid = aHiddenSigmoid;
		outputSigmoid = aOutputSigmoid;

		OutputError = new double[OutputLayer.length];
		HiddenError = new double[HiddenLayer.length];
	}
	
	private void initializeWeights() {
		for(i = 0; i < IHWeights.length; i++){
			for(j = 0; j < IHWeights[i].length; j++){
				IHWeights[i][j] = (Math.random() *2) -1;
			}
		}
		for(i = 0; i < HOWeights.length; i++){
			for(j = 0; j < HOWeights[i].length; j++){
				HOWeights[i][j] = (Math.random() *2) -1;
			}
		}
	}
	
	public void setLearningSpeed(double aLearningSpeed){
		learningSpeed = aLearningSpeed;
	}
	
	public double getLearningSpeed(){
		return learningSpeed;
	}
	
	public double[] propagateForward(double[] input){
		if(input.length == (InputLayer.length -1)){

			for(i = 0; i < InputLayer.length -1; i++){
				InputLayer[i] = inputActivation(input[i]);
			}
			
			if(isBiased){
				InputLayer[InputLayer.length -1] = 1;
			}
			else{
				InputLayer[InputLayer.length -1] = 0;
			}
			
			for(i = 0; i < HiddenLayer.length -1; i++){
				HiddenLayer[i] = 0;
				for(j = 0; j < InputLayer.length; j++){
					HiddenLayer[i] += (double) InputLayer[j] * (double) IHWeights[j][i];
				}
				HiddenLayer[i] = hiddenActivation(HiddenLayer[i]);
			}
			if(isBiased){
				HiddenLayer[HiddenLayer.length -1] = 1;
			}
			else{
				HiddenLayer[HiddenLayer.length -1] = 0;
			}
			
			for(i = 0; i < OutputLayer.length; i++){
				OutputLayer[i] = 0;
				for(j = 0; j < HiddenLayer.length; j++){
					OutputLayer[i] += (double) HiddenLayer[j] * (double) HOWeights[j][i]; 
				}
				OutputLayer[i] = outputActivation(OutputLayer[i]);
			}
			
			return OutputLayer;
		}
		else{
			System.err.println("Size of input in PropagateForward doesnt match network inputlayer size");
			return null;
		}
	}
	
	private double inputActivation(double y) {
		return y;
	}
	
	private double inputActivationDerivative(double y) {
		return 1;
	}

	private double hiddenActivation(double y) {
		if(hiddenSigmoid){
			return (double) 1  /  (double) (1 + Math.pow(Math.E, -y));
		}
		else{
			return y;
		}
	}
	
	private double hiddenActivationDerivative(double y) {
		if(hiddenSigmoid){
			return y*(1-y);
		}
		else{
			return 1;
		}
	}

	private double outputActivation(double y) {
		if(outputSigmoid){
			return (double) 1 / (double) (1 + Math.pow(Math.E, -y));
		}
		else{
			return y;
		}
	}
	
	private double outputActivationDerivative(double y) {
		if(outputSigmoid){
			return y*(1-y);
		}
		else{
			return 1;
		}
	}

	public String toString(){
		String result = new String();
		for(int i = 0; i < IHWeights.length -1; i++){
			for(int j = 0; j < IHWeights[i].length; j++){
				result += "i" + i + "h" + j + ":" + IHWeights[i][j] + "; ";
			}
		}
		if(isBiased){
			for(int j = 0; j < IHWeights[IHWeights.length-1].length; j++){
				result += "b"+ "h" + j + ":" + IHWeights[IHWeights.length-1][j] + "; "; 
			}
		}
		result += '\n';
		for(int i = 0; i < HOWeights.length -1; i++){
			for(int j = 0; j < HOWeights[i].length; j++){
				result += "h" + i + "o" + j + ":" + HOWeights[i][j] + "; "; 
			}
		}
		if(isBiased){
			for(int j = 0; j < HOWeights[HOWeights.length-1].length; j++){
				result += "b" + "o" + j + ":" + HOWeights[HOWeights.length-1][j] + "; "; 
			}
		}
		return result;	
	}
	
	public void save(String fileName) { 
		try { 
			FileOutputStream MyFileStream = new FileOutputStream(fileName); 
			ObjectOutputStream MyObjectStream = new ObjectOutputStream(MyFileStream); 
			MyObjectStream.writeObject(this); 
			MyObjectStream.close(); 
			System.out.println("Saved NNet " + fileName); 
		} catch (Exception excp) { 
			excp.printStackTrace();
		} 
	}
	
	public static ThreeLayerNNet restore(String fileName) {
		ThreeLayerNNet result = null;
		try { 
			FileInputStream MyFileStream = new FileInputStream(fileName); 
			ObjectInputStream MyObjectStream = new ObjectInputStream(MyFileStream); 
			result = (ThreeLayerNNet)MyObjectStream.readObject(); 
		} catch (Exception excp) { 
			excp.printStackTrace();  
		}
		System.out.println("Restored NNet " + fileName);
		return result;
	}
	
	
	public double[] propagateForwardAndBack(double[] input, double[] target){
		double[] output = propagateForward(input);
		if(output != null){
			if(target.length == OutputLayer.length){
				//calculate errors
				//  output error is NIET y - t (5.54) NEE de output error is h'(a(j)) * (y(j)-t(j)) WANT boek gaat uit van geen activation functie op output neuron
				//	hidden error zijn s(j) = h'(a(j))* SUM(w(kj)*s(k)) (5.56) 
				
				for(i = 0; i < OutputLayer.length; i++){
					//OutputError[i] = outputActivationDerivative(OutputLayer[i])*(OutputLayer[i] - target[i]);
					OutputError[i] =  target[i] - OutputLayer[i] ;
				}
				
				for(i = 0; i < HiddenLayer.length -1; i++){
					HiddenError[i] = 0;
					for(j = 0; j < OutputLayer.length; j++){
						HiddenError[i] =+ HOWeights[i][j]*OutputError[j];
					}
					HiddenError[i] *= hiddenActivationDerivative(HiddenLayer[i]); 
				}
				
				//adapt weights:
				//  gebruik de errors en formule 5.53 om de Error wrt weights te berekenen 
				//	en trek die af  van de weights (5.43) 
				for(i = 0; i < IHWeights.length-1; i++){
					for(j = 0; j < IHWeights[i].length; j++){
						if (InputLayer[i] > 0) IHWeights[i][j] = IHWeights[i][j] + (learningSpeed*HiddenError[j]*InputLayer[i]);
					}
				}
				
				if (isBiased)
				{
					for(j = 0; j < IHWeights[IHWeights.length-1].length; j++){
						IHWeights[IHWeights.length-1][j] += (learningSpeed*HiddenError[j]);
					}
				}
				
				for(i = 0; i < HOWeights.length-1; i++){
					for(j = 0; j < HOWeights[i].length; j++){
						if (HiddenLayer[i] > 0) HOWeights[i][j] = HOWeights[i][j] + (learningSpeed*OutputError[j]*HiddenLayer[i]);
					}
				}

				if (isBiased)
				{
					for(j = 0; j < HOWeights[HOWeights.length-1].length; j++){
						HOWeights[HOWeights.length-1][j] += (learningSpeed*OutputError[j]);
					}
				}

				
				
				return output;
			}
			else{
				System.err.println("size of target in PropagateBack doesnt match network outputlayer size");
				return null;
			}
		}
		else{
			return null;
		}
	}

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		integerTimes();
	}

	static void xor(){
		ThreeLayerNNet n = new ThreeLayerNNet(2,2,1,0.02, true,true,true);
		
		double[] in = new double[2];
		double[] target = new double[1];
		double[] out = new double[1];
		for(int i =0;i < 40000;i++){
			in[0] = (int)Math.floor(Math.random()*2);
			in[1] = (int)Math.floor(Math.random()*2);
			target[0] = (int)in[0] ^ (int)in[1];
			out = n.propagateForwardAndBack(in, target);
		}
		
		in[0] = 0;
		in[1] = 0;
		target[0] = (int)in[0] ^ (int)in[1];
		out = n.propagateForward(in);
		System.out.println(in[0] + " XOR " + in[1] + " = " + out[0] + " (" + target[0] + ")");

		in[0] = 0;
		in[1] = 1;
		target[0] = (int)in[0] ^ (int)in[1];
		out = n.propagateForward(in);
		System.out.println(in[0] + " XOR " + in[1] + " = " + out[0] + " (" + target[0] + ")");

		in[0] = 1;
		in[1] = 0;
		target[0] = (int)in[0] ^ (int)in[1];
		out = n.propagateForward(in);
		System.out.println(in[0] + " XOR " + in[1] + " = " + out[0] + " (" + target[0] + ")");

		in[0] = 1;
		in[1] = 1;
		target[0] = (int)in[0] ^ (int)in[1];
		out = n.propagateForward(in);
		System.out.println(in[0] + " XOR " + in[1] + " = " + out[0] + " (" + target[0] + ")");

	}
	
	static void decimalTimes(){
		double[] in = new double[2];
		double[] target = new double[1];
		double[] out = new double[1];
		for(int t = 0; t < 50; t++){
			ThreeLayerNNet n = new ThreeLayerNNet(2,4,1,0.1,true,true,false);
			for(int i =0;i < 1000000;i++){
				in[0] = Math.random();
				in[1] = Math.random();
				target[0] = in[0] * in[1];
				out = n.propagateForwardAndBack(in, target);
			}
		
			double error = 0;
			final int iter = 100000;
			for(int i =0;i < iter;i++){
				in[0] = Math.random();
				in[1] = Math.random();
				target[0] = in[0] * in[1];
				out = n.propagateForward(in);
				//System.out.println(in[0] + "*" + in[1] + "=" + out[0] + " (" + target[0] +")");
				error = error + (Math.pow(target[0] - out[0], 2));
			}
			System.out.println("error = " + error/iter);
		}
	}
	
	static void integerTimes(){
		ThreeLayerNNet n = new ThreeLayerNNet(2,5,1,0.05,true,true,false);
		
		final int MAX_DIGITS = 2;
		
		double[] in = new double[2];
		double[] target = new double[1];
		double[] out = new double[1];
		for(int i =0;i < 100000000;i++){
			in[0] = (Math.floor(Math.random()* Math.pow(10,MAX_DIGITS)))/Math.pow(10,MAX_DIGITS);
			in[1] = (Math.floor(Math.random()* Math.pow(10,MAX_DIGITS)))/Math.pow(10,MAX_DIGITS);
			target[0] = in[0] * in[1];
			out = n.propagateForwardAndBack(in, target);
		}
		
		double error = 0;
		for(int i =0;i < Math.pow(10,MAX_DIGITS);i++){
			for(int j = 0; j < Math.pow(10,MAX_DIGITS);j++){
				in[0] = i/Math.pow(10,MAX_DIGITS);
				in[1] = j/Math.pow(10,MAX_DIGITS);
				target[0] = in[0] * in[1];
				out = n.propagateForward(in);
				int integerIn0 = (int)(in[0]*Math.pow(10,MAX_DIGITS));
				int integerIn1 = (int)(in[1]*Math.pow(10,MAX_DIGITS));
				int integerOut = (int)(Math.round(out[0]*Math.pow(10,MAX_DIGITS*2)));
				int integerTarget = (int)(Math.round(target[0]*Math.pow(10,MAX_DIGITS*2)));
				System.out.println(integerIn0 + "*" + integerIn1 + "=" + integerOut+ " (" + integerTarget +")");
				if(integerOut != integerTarget){
					error += 1;
				}
			}
		
		}
		System.out.println("error = " + (1 -(error/Math.pow(10, MAX_DIGITS*2))));
	}
	
	static void decimalAdd(){
		double[] in = new double[2];
		double[] target = new double[1];
		double[] out = new double[1];
		ThreeLayerNNet n = new ThreeLayerNNet(2,1,1,0.02,false,false,false);
		for(int i =0;i < 1000000;i++){
			in[0] = Math.random()/2;
			in[1] = Math.random()/2;
			target[0] = in[0] + in[1];
			out = n.propagateForwardAndBack(in, target);
		} 
			
		double error = 0;
		final int iter = 100;
		for(int i =0;i < iter;i++){
			in[0] = Math.random()/2;
			in[1] = Math.random()/2;
			target[0] = in[0] + in[1];
			out = n.propagateForward(in);
			System.out.println(in[0] + "+" + in[1] + "=" + out[0] + " (" + target[0] +")");
			error = error + (Math.pow(target[0] - out[0], 2));
		}
		System.out.println("error = " + error/iter);
	}
}
